// SPDX-License-Identifier: GPL-2.0
/*
 * Driver for LomboTech nx platform usb2 glue
 *
 * Copyright (C) 2016-2022, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <linux/module.h>
#include <linux/of_platform.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/dma-mapping.h>
#include <linux/clk.h>
#include <linux/of_device.h>
#include <linux/regmap.h>
#include <linux/mfd/syscon.h>
#include <linux/of_reserved_mem.h>

#ifdef CONFIG_PM_GENERIC_DOMAINS
#include <linux/pm_clock.h>
#endif	/* CONFIG_PM_GENERIC_DOMAINS */

#include "lb.h"
#include "lb_hdc_nx.h"
#include "lb_hdc_n7v3.h"
#include "lb_hdc_n7v5.h"
#include "lb_hdc_n7v7.h"

/* default offset of capability registers */
#define DEF_CAPOFFSET			0x100

/* for dts gpio */
#define OF_GPIO_NUM_ERROR		-1

#define OF_GPIO_VBUS			"vbus-enable-gpios"
#define OF_GPIO_ROLE_PORT		"host-port-enable-gpios"

/* for dts clk */
#define OF_CLK_USB_GATE			"usb_gate"
#define OF_CLK_USB_RESET		"usb_reset"

typedef struct lb_hdc_soc_data *(*lb_hdc_nx_get_soc_data)(void);

struct lb_hdc_nx {
	struct device			*dev;
	struct clk			*clk_gate;
	struct clk			*clk_reset;

	struct usb_phy			*usb_phy;

	struct platform_device		*lb_pdev;

	bool				in_lpm;
	bool				supports_runtime_pm;

	/* dts gpio */
	int				gpio_vbus;
	u8				vbus_enable;
	int				gpio_role_port;
	u8				host_port_enable;

	struct resource			*res;
	void __iomem			*reg_base;

	struct lb_hdc_soc_data		*soc_data;
};

static const struct of_device_id lb_hdc_nx_dt_ids[] = {
	{ .compatible = "lombo,n7v3-usb", .data = lb_hdc_n7v3_get_data },
	{ .compatible = "lombo,n7v5-usb", .data = lb_hdc_n7v5_get_data },
	{ .compatible = "lombo,n7v7-usb", .data = lb_hdc_n7v7_get_data },

	{},
};
MODULE_DEVICE_TABLE(of, lb_hdc_nx_dt_ids);

static int lb_hdc_nx_host_start(struct lb_hdc_nx *nx_hdc)
{
	int ret;
	int gpio;

	gpio = nx_hdc->gpio_role_port;
	if (gpio >= 0) {
		ret = gpio_direction_output(gpio, nx_hdc->host_port_enable);
		if (ret < 0)
			dev_warn(nx_hdc->dev,
				 "%s set gpio role port failed!\n", __func__);
	}

	gpio = nx_hdc->gpio_vbus;
	if (gpio >= 0) {
		ret = gpio_direction_output(gpio, nx_hdc->vbus_enable);
		if (ret < 0)
			dev_warn(nx_hdc->dev,
				 "%s set gpio vbus failed!\n", __func__);
	}

	return 0;
}

static void lb_hdc_nx_host_stop(struct lb_hdc_nx *nx_hdc)
{
	int ret;
	int gpio;

	gpio = nx_hdc->gpio_vbus;
	if (gpio >= 0) {
		ret = gpio_direction_output(gpio, !nx_hdc->vbus_enable);
		if (ret < 0)
			dev_warn(nx_hdc->dev,
				 "%s set gpio vbus failed!\n", __func__);
	}
}

static int lb_hdc_nx_gadget_start(struct lb_hdc_nx *nx_hdc)
{
	int ret;
	int gpio;

	gpio = nx_hdc->gpio_role_port;
	if (gpio >= 0) {
		ret = gpio_direction_output(gpio, !nx_hdc->host_port_enable);
		if (ret < 0)
			dev_warn(nx_hdc->dev,
				 "%s set gpio role port failed!\n", __func__);
	}

	gpio = nx_hdc->gpio_vbus;
	if (gpio >= 0) {
		ret = gpio_direction_output(gpio, !nx_hdc->vbus_enable);
		if (ret < 0)
			dev_warn(nx_hdc->dev,
				 "%s set gpio vbus failed!\n", __func__);
	}

	return 0;
}

static void lb_hdc_nx_gadget_stop(struct lb_hdc_nx *nx_hdc)
{
	/* TODO! */
}

static int lb_hdc_nx_notify_event(struct lb_hdc *lb, unsigned int event)
{
	int ret = 0;
	struct lb_hdc_nx *nx_hdc = lb->platdata->soc_hdc;
	struct device *dev = nx_hdc->dev;

	switch (event) {
	case LB_HDC_USB_HOST_START:
		lb_hdc_nx_host_start(nx_hdc);
		break;
	case LB_HDC_USB_HOST_STOP:
		lb_hdc_nx_host_stop(nx_hdc);
		break;
	case LB_HDC_USB_GADGET_START:
		lb_hdc_nx_gadget_start(nx_hdc);
		break;
	case LB_HDC_USB_GADGET_STOP:
		lb_hdc_nx_gadget_stop(nx_hdc);
		break;

	default:
		dev_info(dev, "unsupport event %d\n", event);
	}

	return ret;
}

static void lb_hdc_nx_get_of_gpio(struct lb_hdc_nx *nx_hdc)
{
	int			ret = 0;
	int			gpio;
	enum of_gpio_flags	gpio_flags;
	char			gpio_name[60];
	struct device		*dev = nx_hdc->dev;
	struct device_node	*np = dev->of_node;

	/* set gpios default to invalid */
	nx_hdc->gpio_vbus	= OF_GPIO_NUM_ERROR;
	nx_hdc->gpio_role_port	= OF_GPIO_NUM_ERROR;

	/* usb vbus gpio */
	gpio = of_get_named_gpio_flags(np, OF_GPIO_VBUS, 0, &gpio_flags);
	if (gpio >= 0) {
		memset(gpio_name, 0x00, sizeof(gpio_name));
		sprintf(gpio_name, "%s-vbus", dev_name(dev));
		ret = devm_gpio_request(dev, gpio, gpio_name);
		if (ret < 0) {
			dev_err(dev, OF_GPIO_VBUS " gpio request err!\n");
		} else {
			nx_hdc->gpio_vbus = gpio;
			nx_hdc->vbus_enable = gpio_flags;
		}
	}

	/* usb role port sel gpio */
	gpio = of_get_named_gpio_flags(np, OF_GPIO_ROLE_PORT, 0, &gpio_flags);
	if (gpio >= 0) {
		memset(gpio_name, 0x00, sizeof(gpio_name));
		sprintf(gpio_name, "%s-role-port", dev_name(dev));
		ret = devm_gpio_request(dev, gpio, gpio_name);
		if (ret < 0) {
			dev_err(dev, OF_GPIO_ROLE_PORT " request err!\n");
		} else {
			nx_hdc->gpio_role_port = gpio;
			nx_hdc->host_port_enable = gpio_flags;
		}
	}
}

static int lb_hdc_nx_clk_init(struct lb_hdc_nx *nx_hdc)
{
	int ret = 0;
	struct device *dev = nx_hdc->dev;

#if IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)
	nx_hdc->clk_gate = clk_get(dev, OF_CLK_USB_GATE);
	if (IS_ERR(nx_hdc->clk_gate)) {
		dev_err(dev, "get usb gate clk failed!\n");
		ret = -EINVAL;
		goto error;
	}

	ret = pm_clk_add_clk(dev, nx_hdc->clk_gate);
	if (ret) {
		dev_err(dev, "%s: pm add usb clk gate failed!\n", __func__);
		ret = -EINVAL;
		clk_put(nx_hdc->clk_gate);
		goto error;
	}

	/* resume clk gate */
	ret = pm_clk_resume(dev);
	if (ret) {
		dev_err(dev, "%s: pm resume usb clk gate failed!\n", __func__);
		ret = -EINVAL;
		pm_clk_remove_clk(dev, nx_hdc->clk_gate);
		goto error;
	}

#else
	nx_hdc->clk_gate = devm_clk_get(dev, OF_CLK_USB_GATE);
	if (IS_ERR(nx_hdc->clk_gate)) {
		dev_err(dev, "get usb gate clk failed!\n");
		goto error;
	}

	/* enable clk gate */
	ret = clk_prepare_enable(nx_hdc->clk_gate);
	if (ret) {
		dev_err(dev, "%s: enable usb clk gate failed!\n", __func__);
		goto error;
	}
#endif

	nx_hdc->clk_reset = devm_clk_get(dev, OF_CLK_USB_RESET);
	if (IS_ERR(nx_hdc->clk_reset)) {
		dev_err(dev, "get usb reset clk failed!\n");
		goto deinit_clk_gate;
	}

	/* enable clk reset */
	ret = clk_prepare_enable(nx_hdc->clk_reset);
	if (ret) {
		dev_err(dev, "%s: enable usb clk reset failed!\n", __func__);
		goto deinit_clk_gate;
	}

	return 0;

deinit_clk_gate:
#if IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)
	pm_clk_suspend(dev);
	pm_clk_remove_clk(dev, nx_hdc->clk_gate);
#else
	clk_disable_unprepare(nx_hdc->clk_gate);
#endif

error:
	return ret;
}

static void lb_hdc_nx_clk_deinit(struct lb_hdc_nx *nx_hdc)
{
#if IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)
	struct device *dev = nx_hdc->dev;

	if (nx_hdc->clk_gate) {
		pm_clk_suspend(dev);
		pm_clk_remove_clk(dev, nx_hdc->clk_gate);
	}
#else
	if (nx_hdc->clk_gate)
		clk_disable_unprepare(nx_hdc->clk_gate);
#endif

	if (nx_hdc->clk_reset)
		clk_disable_unprepare(nx_hdc->clk_reset);
}

/*
 * lb hdc nx runtime pm
 */
static void lb_hdc_nx_pm_runtime_init(struct lb_hdc_nx *nx_hdc)
{
	struct device *dev = nx_hdc->dev;

#if IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)
	pm_runtime_set_active(dev);
	pm_runtime_enable(dev);
	/* usb don't support close the clk gate runtime at present */
	pm_runtime_get_noresume(dev);
#else
	pm_runtime_set_active(dev);
	pm_runtime_enable(dev);
#endif
}

static void lb_hdc_nx_pm_runtime_deinit(struct lb_hdc_nx *nx_hdc)
{
	struct device *dev = nx_hdc->dev;

#if IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)
	pm_runtime_put_sync(dev);
	pm_runtime_disable(dev);
#else
	pm_runtime_get_sync(dev);
	pm_runtime_disable(dev);
	pm_runtime_put_noidle(dev);
#endif
}

static int lb_hdc_nx_probe(struct platform_device *pdev)
{
	int					ret;
	struct lb_hdc_nx			*nx_hdc;
	lb_hdc_nx_get_soc_data			get_soc_data_func;
	const struct of_device_id		*of_id;
	struct lb_hdc_platform_data		pdata = {
		.name		= dev_name(&pdev->dev),
		.capoffset	= DEF_CAPOFFSET,
		.notify_event	= lb_hdc_nx_notify_event,
	};

	dev_info(&pdev->dev, "%s start...", __func__);

	of_id = of_match_device(lb_hdc_nx_dt_ids, &pdev->dev);
	if (!of_id) {
		dev_err(&pdev->dev, "of_match_device failed!\n");
		return -ENODEV;
	}

	if (!of_id->data) {
		dev_err(&pdev->dev, "of-data is NULL!\n");
		return -EINVAL;
	}
	get_soc_data_func = of_id->data;

	nx_hdc = devm_kzalloc(&pdev->dev, sizeof(*nx_hdc), GFP_KERNEL);
	if (!nx_hdc) {
		dev_err(&pdev->dev, "alloc nx_hdc failed!\n");
		return -ENOMEM;
	}
	nx_hdc->dev = &pdev->dev;
	platform_set_drvdata(pdev, nx_hdc);

	/* get soc data */
	nx_hdc->soc_data = get_soc_data_func();
	if (!nx_hdc->soc_data) {
		dev_err(&pdev->dev, "get soc data failed!\n");
		return -EINVAL;
	}

	/* clk init */
	ret = lb_hdc_nx_clk_init(nx_hdc);
	if (ret) {
		dev_err(&pdev->dev, "enable usb clk failed!\n");
		return ret;
	}

	/*
	 * dts parse
	 */

	/* gpio */
	lb_hdc_nx_get_of_gpio(nx_hdc);

	/* register address */
	nx_hdc->res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	nx_hdc->reg_base = devm_ioremap_resource(&pdev->dev, nx_hdc->res);
	if (IS_ERR(nx_hdc->reg_base)) {
		dev_err(&pdev->dev, "err ioremap invalid\n");
		goto clk_deinit;
	}

	/* phy */
	nx_hdc->usb_phy = devm_usb_get_phy_by_phandle(&pdev->dev,
						      "lombo,usbphy",
						      0);
	if (IS_ERR(nx_hdc->usb_phy)) {
		/* Return -EINVAL if no usbphy is available */
		ret = -EINVAL;
		dev_err(&pdev->dev, "devm_usb_get_phy_by_phandle failed!\n");
		goto clk_deinit;
	}

	ret = dma_coerce_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
	if (ret) {
		dev_err(&pdev->dev, "dma_coerce_mask_and_coherent failed!\n");
		goto clk_deinit;
	}

	pdata.soc_hdc		= nx_hdc;
	pdata.res		= nx_hdc->res;
	pdata.reg_base		= nx_hdc->reg_base;
	pdata.usb_phy		= nx_hdc->usb_phy;
	pdata.soc_data		= nx_hdc->soc_data;

	nx_hdc->lb_pdev = lb_hdc_add_device(&pdev->dev, pdev->resource,
					    pdev->num_resources, &pdata);
	if (IS_ERR(nx_hdc->lb_pdev)) {
		ret = PTR_ERR(nx_hdc->lb_pdev);
		dev_err(&pdev->dev, "register platform device err %d\n", ret);
		goto clk_deinit;
	}

	if (nx_hdc->soc_data->flags & LB_HDC_SUPPORTS_RUNTIME_PM) {
		nx_hdc->supports_runtime_pm = true;
		lb_hdc_nx_pm_runtime_init(nx_hdc);
	}

	dev_info(&pdev->dev, "%s done.", __func__);

	return 0;

clk_deinit:
	lb_hdc_nx_clk_deinit(nx_hdc);

	dev_err(&pdev->dev, "%s err %d\n", __func__, ret);

	return ret;
}

static int lb_hdc_nx_remove(struct platform_device *pdev)
{
	struct lb_hdc_nx *nx_hdc = platform_get_drvdata(pdev);

	if (nx_hdc->supports_runtime_pm)
		lb_hdc_nx_pm_runtime_deinit(nx_hdc);

	lb_hdc_nx_clk_deinit(nx_hdc);

	lb_hdc_remove_device(nx_hdc->lb_pdev);

	return 0;
}

#ifdef CONFIG_PM
static int nx_controller_suspend(struct device *dev)
{
	struct lb_hdc_nx *nx_hdc = dev_get_drvdata(dev);

	/* todo! release or disable clk? */
	nx_hdc->in_lpm = true;

	return 0;
}

static int nx_controller_resume(struct device *dev)
{
	struct lb_hdc_nx *nx_hdc = dev_get_drvdata(dev);

	if (!nx_hdc->in_lpm) {
		WARN_ON(1);
		return 0;
	}

	/* todo! prepare and enable clk? */
	nx_hdc->in_lpm = false;

	return 0;
}

static void lb_hdc_nx_suspend_clk(struct lb_hdc_nx *nx_hdc)
{
	clk_disable_unprepare(nx_hdc->clk_reset);

#if IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)
	/* todo! */
#else
	clk_disable_unprepare(nx_hdc->clk_gate);
#endif
}

static int lb_hdc_nx_resume_clk(struct lb_hdc_nx *nx_hdc)
{
	int ret = 0;
	struct device *dev = nx_hdc->dev;

#if IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)
	/* todo! */
#else
	ret = clk_prepare_enable(nx_hdc->clk_gate);
	if (ret) {
		dev_err(dev, "%s: enable usb clk gate failed\n", __func__);
		return ret;
	}
#endif

	ret = clk_prepare_enable(nx_hdc->clk_reset);
	if (ret) {
		dev_err(dev, "%s: enable usb clk reset failed\n", __func__);
		goto disable_clk_gate;
	}

	return 0;

disable_clk_gate:
#if IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)
	pm_clk_remove_clk(dev, nx_hdc->clk_gate);
#else
	clk_disable_unprepare(nx_hdc->clk_gate);
#endif

	return ret;
}

#ifdef CONFIG_PM_SLEEP
static int lb_hdc_nx_suspend(struct device *dev)
{
	int ret;
	struct lb_hdc_nx *nx_hdc = dev_get_drvdata(dev);

	if (nx_hdc->in_lpm)
		/* The core's suspend doesn't run */
		return 0;

	/*controller suspend*/
	ret = nx_controller_suspend(dev);
	if (ret) {
		dev_err(dev, "controller suspend failed\n");
		return ret;
	}

	lb_hdc_nx_suspend_clk(nx_hdc);

	return 0;
}

static int lb_hdc_nx_resume(struct device *dev)
{
	int ret;
	struct lb_hdc_nx *nx_hdc = dev_get_drvdata(dev);

	ret = lb_hdc_nx_resume_clk(nx_hdc);
	if (ret)
		goto out;

	/*controller resume*/
	ret = nx_controller_resume(dev);
	if (!ret && nx_hdc->supports_runtime_pm) {
		if (!IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)) {
			pm_runtime_disable(dev);
			pm_runtime_set_active(dev);
			pm_runtime_enable(dev);
		}
	}

out:
	return ret;
}
#endif /* CONFIG_PM_SLEEP */

static int lb_hdc_nx_runtime_suspend(struct device *dev)
{
	struct lb_hdc_nx *nx_hdc = dev_get_drvdata(dev);

	if (nx_hdc->in_lpm) {
		WARN_ON(1);
		return 0;
	}

	return nx_controller_suspend(dev);
}

static int lb_hdc_nx_runtime_resume(struct device *dev)
{
	return nx_controller_resume(dev);
}

#endif /* CONFIG_PM */

static const struct dev_pm_ops lb_hdc_nx_pm_ops = {
	SET_SYSTEM_SLEEP_PM_OPS(lb_hdc_nx_suspend, lb_hdc_nx_resume)
	SET_RUNTIME_PM_OPS(lb_hdc_nx_runtime_suspend,
			   lb_hdc_nx_runtime_resume,
			   NULL)
};

static struct platform_driver lb_hdc_nx_driver = {
	.probe = lb_hdc_nx_probe,
	.remove = lb_hdc_nx_remove,
	.driver = {
		.name = "nx_usb",
		.owner = THIS_MODULE,
		.of_match_table = of_match_ptr(lb_hdc_nx_dt_ids),
		.pm = &lb_hdc_nx_pm_ops,
	 },
};

static int __init lb_hdc_nx_init(void)
{
	return platform_driver_register(&lb_hdc_nx_driver);
}

subsys_initcall_sync(lb_hdc_nx_init);

MODULE_ALIAS("platform:nx-usb");
MODULE_AUTHOR("lomboswer@lombotech.com");
MODULE_DESCRIPTION("Driver for LomboTech nx platform usb2 glue");
MODULE_LICENSE("GPL");
